Apple, the Apple logo, and Macintosh are registered trademarks of Apple Computer, Inc.
Mac and OpenDoc are trademarks of Apple Computer, Inc.
The Except utility library implements a simple catch/throw style exception handling scheme; it’s quite similar to the ones found in MacApp, TCL and PowerPlant. (It is not as spiffy (or as heavyweight) as the ODF exception library.) To use this utility, you must link the file Except.cpp into your library, and include the header Except.h in your source files before the headers of any SOM™ classes.
The use of this utility is optional; however, if you don’t use it you must check the environment (ev or fEv) after every SOM call you make and handle it appropriately, propagating the error back to your caller. Page 3-28 of the SOM manual describes this in more detail.
In a catch/throw exception system, an error is signaled by being thrown. The stack unwinds back to the point where a calling function set up an error handler, and this handler then catches the error. The handler can then do whatever clean-up is necessary. It can then continue on, or (more commonly) reraise the error, which throws it back to the next exception handler on the stack.
If you are already experienced with this style of exception handling, you may want to skim through the following sections until Exceptions And SOM which describes features unique to this exception package.
Setting Up the Build System
You’ll need to add the utility file Except.cpp into your build system — this might involve adding the file to your project if you use CodeWarrior, Symantec C++, or adding it to a makefile if you use MPW.
You also need to include the header file Except.h before including any ".xh" files. Except.h defines some magic stuff that activates the auto-throw behavior in SOM methods so that an error returned from a method call will automatically result in an exception being thrown. If you don't include Except.h first, nothing in particular will happen when a method you call returns an error. This is usually taken care of in your precompiled header, but if you're starting a project from scratch you'll need to make sure you're doing this.
If you're building your project in debug mode (the symbol ODDebug is defined as 1), then Except.cpp will call functions from Crawl.cpp, and you'll need to add that source file to your project or makefile. Crawl.cpp in turn depends on ToolLibs.o (68k) or PPCToolsLib.o (PPC), which are part of your development system.
Example of Use
Here’s an example of the most basic use of the exception system; we’re ignoring its interaction with SOM for now.
ODHandle MyNewHandle( ODSize size )
{
OSErr err;
ODHandle h = NewTempHandle(size,&err);
THROW_IF_ERROR(err);
return h;
}
void foo( )
{
Handle h1, h2;
h1 = MyNewHandle(10000);
TRY{
h2 = MyNewHandle(10000);
}CATCH_ALL{
ODDisposeHandle(h1);
RERAISE;
}ENDTRY
...
}
This example shows a fail-safe allocation of two handles. MyNewHandle allocates memory and throws an exception if the memory allocation fails. (THROW_IF_ERROR is part of the exception system; it throws an exception if the OSErr passed in is nonzero.) If the first call to MyNewHandle fails, it throws an error. Since foo has not set up an exception handler (a TRY … CATCH_ALL … ENDTRY block) around that call, the exception will be thrown out of foo into its caller, and so on up the stack until an exception handler is found.
If the first call succeeds, we come to the second call to MyNewHandle, which is inside an exception handler. If this call throws an exception, it will be caught by the exception handler, and the block after CATCH_ALL will run. The purpose of this block is to clean up by disposing the handle h1, preventing a memory leak. After h1 is disposed, it calls RERAISE, which reraises the same exception, which then goes up the stack until the next enclosing exception handler is found. (If we hadn’t called RERAISE, we would have fallen out of the exception handler to the statement after ENDTRY.)
If the second call to MyNewHandle succeeds, we fall out of the entire exception handler, skipping the CATCH_ALL block entirely (since there was no exception) and ending up at the statement immediately following ENDTRY.
Raising Exceptions
An exception is thrown by calling THROW or one of its variants. This immediately jumps to the most recently installed exception handler, as described above.
THROW( ODError error )
Throws the exception whose value is given by error. There is a list of standard OpenDoc error codes in ErrorDef.xh; platform/OS errors can also be thrown where there is no OpenDoc equivalent. Error must be nonzero; it doesn’t make any sense to throw a kODNoError exception!
THROW_IF_NULL( void* ptr )
Throws the exception kODErrOutOfMemory if a null pointer is input. This function is intended to be called after you call a memory allocator (such as SOMNew or MMNewPtr) which returns NULL if there’s not enough memory available.
It should not be used with a function that may return NULL for other reasons: for instance, the Mac Toolbox routine GetResource may return NULL if the resource could not be found; after calling it you should call ResError to find the actual error code.
In C++ you can add an optional error code after the ptr parameter; this will cause that error to be thrown rather than kODErrOutOfMemory if the pointer is NULL. This is handy in situations where a NULL pointer means something different (such as a missing resource.)
THROW_IF_ERROR( ODError error )
Throws an exception if error is non-zero (not equal to kODNoError.) Otherwise nothing happens. This is a useful call to wrap around a function call that returns an error code; a Mac example is:
THROW_IF_ERROR( FSpOpenDF(&fileSpec,&refNum) );
FSpOpenDF returns zero if it succeeds, otherwise a nonzero OSErr code. Passing the result to THROW_IF_ERROR ensures that the right exception will be thrown if the call failed.
Exception Handlers
An exception handler consists of a TRY block, zero or more CATCH blocks, an optional CATCH_ALL block, and ends with an ENDTRY:
TRY{
statements
}CATCH_ALL{
statements
RERAISE // optional
}ENDTRY
(The curly braces are not strictly necessary but are encouraged for readability, and for convenience when using editors that know how to find matching braces for you.)
TRY
The statements are executed. If one of the statements, or any function one of the statements calls, throws an exception that reaches this exception handler, then one of the following CATCH or CATCH_ALL blocks will be called. Otherwise, after the last statement finishes, control passes to the statement following the ENDTRY.
In C, you cannot return, break or otherwise jump out of a TRY block. See the Caveats section below.
CATCH_ALL
If any exception is caught by this handler, the following statements are executed. If RERAISE is called, the same exception will be re-raised, thrown to the next exception handler on the stack. (Similarly, if any statement in the CATCH_ALL block throws an exception, it will be caught by the next exception handler.) Otherwise, after the last statement finishes, control passes to the statement following the ENDTRY.
ENDTRY
Indicates the end of an exception handler. After a TRY or CATCH block finishes without throwing or reraising an exception, the exception handler removes itself from the stack and control passes to the statement following ENDTRY.
RERAISE
Called within a CATCH_ALL block, causes the exception to be thrown again, to the next active exception handler on the stack. This is the normal behavior for an exception handler -- most of the time you don't want to hide the error, you want to propagate it so a higher level handler can deal with it.
GetErrorCode
Within a CATCH_ALL block, returns the error code of the exception. You can use this to detect which error was thrown and act accordingly.
SetErrorCode
Within a CATCH_ALL block, changes the error code of the exception. RERAISE-ing the exception will then throw the new error code.
SetErrorMessage
Within a CATCH_ALL block, attaches a C string to the exception as a message. Presumably this is a user-readable string that can be presented in an error alert, if you want.
GetErrorMessage
Returns a pointer to the string, if any, attached to the exception. You do not need to delete this string, but the pointer will become invalid after the exception is reraised or after the ENDTRY.
It’s perfectly legal (and not uncommon) to lexically nest exception handlers in a single function. Any error caught and reraised by the inner handler will be caught by the outer one.
Using Native C++ Exceptions
If your compiler supports built-in/native C++ exceptions (e.g. CodeWarrior 6, Symantec C++ v8) you can make the Except utility use these exceptions — for instance, the TRY and CATCH macros will be defined as 'try' and 'catch' — instead of simulating them via setjmp and longjmp. You can do this by #define'ing the symbol _NATIVE_EXCEPTIONS_. This has to be done before Except.h is first included, which generally means you'll need to define it in a prefix file or in a precompiled header (since Except.h is typically precompiled.)
The benefit of using native exceptions is that you don't have to be careful to obey the Caveats below, and that exceptions can safely be thrown from inside constructors and destructors with no concerns on your part. The disadvantage is that the code generated may not be as small and efficient as emulated exceptions would be (this is the case in CodeWarrior 6, but it may be substantially improved in later versions of CodeWarrior.)
NOTE: Unfortunately, a bug slipped through and couldn't be fixed in time for DR3. This bug causes the macro ErrorMessage() to be defined improperly when using native exceptions. You can fix it easily enough by going to line 156 of Except.h and removing the excess "&", changing it to:
#define ErrorMessage() (_exception.message)
Sorry about this. It will be fixed in the next release.
NOTE: The utilities InfoUtil and StorUtil are not (yet) compatible with native exceptions. Sorry; again, we'll fix it in the next release. Or you could fix it yourself for extra credit.
Caveats
This exception-handling package, if you don't use native exceptions (see above), is grafted onto C/C++ using some tricky macros and library functions. It is therefore “fooling” the compiler to some degree. Because of this, there are some things you have to watch out for to avoid confusing the compiler into generating incorrect code…
Caveat 1: Jumping/returning out of a TRY block (C only)
In C, but not C++, it is illegal to jump out of the block of statements after TRY.
This includes using return, break, continue or goto. The flow of control has to fall out of the end of the TRY block (unless an exception is thrown) otherwise the exception handler won't be deactivated and Very Bad Things will happen.
In C++, the exception handler's destructor is automatically called if you break out of a TRY block. This generates a few extra bytes of code, but is otherwise harmless.
Caveat 2: Volatile Variables
Since the compiler doesn’t fully understand the possible flow of control in a function when exceptions are thrown and caught, its register allocator and optimizer can make incorrect assumptions and generate bad code unless you take a simple precaution:
Any variable or parameter that is modified in a TRY block, and then used in a CATCH or CATCH_ALL block, must be declared as volatile.
[This Caveat does not apply if you are using native exceptions, i.e. you predefined _NATIVE_EXCEPTIONS_. C++ knows enough about its native exception system to avoid the problems that would require variables to be marked volatile. You may still want to obey the restrictions of this caveat if you want your source code to be usable without native exceptions.]]
The reason for this is that the compiler doesn’t understand that the TRY block can be executed on the way to a CATCH block, and therefore that the variable may be modified before the CATCH block is reached. It may therefore end up using an obsolete value for the variable while in the CATCH block. To work around this, you have to tell the compiler not to store the variable’s value in a register (which may be out of date) but always to look it up from the stack frame.
The C++ volatile keyword in the variable declaration does this; but some compilers don’t implement it properly, and it can be confusing to use properly with pointer variables. For this reason the exception system defines a macro ODVolatile that declares a variable to be volatile. All you have to do is put this after the variable declaration. Here’s an example:
void *p = kODNULL; ODVolatile(p);
TRY{
Zog1();
p = ODNewPtr(10000);
Zog2();
}CATCH{
ODDisposePtr(p);
RERAISE;
}ENDTRY
The purpose of the exception handler is to make sure that p is disposed on the way out in case it was allocated by ODNewPtr. Because p is modified inside the TRY block, it has to be marked as volatile. (Note that when the CATCH block is called, p might still be NULL — if Zog1 or ODNewPtr threw the exception — or it might be a valid pointer, if it was Zog2 that threw the exception. Fortunately, we pre-initialized p to kODNULL, and ODDisposePtr can safely be passed a null pointer. If we hadn’t initialized p, this code might crash.)
Caveat 3: Constructors & Destructors
Another caveat follows from the fact that the C++ runtime code that constructs objects does not know about exceptions:
Exceptions may not be thrown out of constructors or destructors except in certain circumstances.
[Again, this caveat does not apply if you are using native C++ exceptions. The C++ standard allows exceptions to be thrown from constructors and destructors. You may still want to obey the restrictions of this caveat if you want your source code to be usable without native exceptions.]
The reason for this is that the exception will jump out of the runtime construction / deletion code straight into the nearest exception handler, leaving the object in an invalid partially-constructed or -destructed state.
It is permissible for a constructor or destructor to throw an exception (or call something that does) as long as it catches the exception itself and does not reraise it. In practice, since constructors cannot return error results, this isn’t very useful; it’s easier if you put initialization code that might fail into a separate Initialize or InitClass method that clients must call immediately after constructing the object. The constructor itself then just initializes the fields of the object to a default state where the destructor can safely be called.
Destructo objects (see below) can throw exceptions from their constructors or destructors, if you are careful. This is discussed in the next section.
Auto-Destructing Stack-Based Objects
In C++, when a block scope is exited through normal means, the destructors (if any) of stack-based objects allocated in that block are called. However, since the exception system fools the compiler, this process doesn’t automatically happen when a block is exited by an exception. The exception library does, however, have its own mechanism for ensuring that this happens, but it requires some extra notification.
If you have a class that will be used only for stack-based objects, and the class has a destructor that must be called whenever the object goes out of scope (perhaps it deletes a memory block or something else important), you should derive that class from the base class Destructo which is provided by the exception library. Destructo objects, when they are constructed, register themselves with the active exception handler so that it will know to destruct them when an exception is thrown.
[If you are using native C++ exceptions, all destructors are automatically called when a block exits due to an exception, whether or not the object inherits from Destructo. You may still want to use the Destructo base class if you want your source code to be usable without native exceptions.]
This allows you to implement a very useful C++ programming idiom, in which the pair of calls to acquire and release some sort of resource (like a memory block, or a reference to a ref-counted object, or the “locked” flag of a handle) are implemented by the constructor and destructor of a stack-based class. This means that you don’t have to remember to make the “release” call since it happens implicitly when the object goes out of scope; when working with exceptions, it means that you can also be confident that, if an exception is thrown, the release will still take place on the way out. This is a lot easier than having to wrap an exception handler around the block.
Example
Here’s an example with and without this idiom. The old way:
void Frob( ODHandle foo ) {
ODLockHandle(foo);
TRY{
Zog(*foo);
}CATCH_ALL{
ODUnlockHandle(foo);
RERAISE;
}ENDTRY
ODUnlockHandle(foo);
}
This is unwieldy and easy to get wrong, and the unlock code has to appear twice. It’s also inefficient at runtime, since TRY is an expensive operation (it involves saving all the CPU registers; PowerPC’s in particular have a lot of registers.) So let’s first create a CHandleLocker utility class:
class CHandleLocker :Destructo {
public:
CHandleLocker( ODHandle );
~CHandleLocker( );
private:
ODHandle fHandle;
}
CHandleLocker::CHandleLocker( ODHandle h )
{ fHandle = h; ODLockHandle(fHandle); }
CHandleLocker::~CHandleLocker( )
{ ODUnlockHandle(fHandle); }
Now here’s the same Frob function using the utility class:
void Frob( Handle foo ) {
CHandleLocker lock(foo);
Zog(*foo);
}
If no exception is thrown by Zog, the call returns normally and the lock object is destructed (unlocking the handle) just before the Frob returns normally. If, however, Zog throws an exception, the exception frame in which the call to Frob appears still calls the destructor of the lock object. Either way, the handle is not accidentally left locked.
Exceptions in Destructo constructors / destructors
A Destructo object (see below) may throw an exception out of its constructor. However, the exception will cause the object to be cleaned up (that's the whole point of Destructos) so the object's destructor will immediately be called. Therefore, you must ensure that, at the point the exception is thrown, the fields of the object are in good shape and the object can be safely destructed.
A Destructo may also throw an exception out of its destructor. Again, the exception will cause the object to be destructed, resulting in a second call to the destructor! If the second call fails in the same way, an infinite loop will result. Therefore, any destructor operation that might fail must first reset the state of the object so that the operation will not happen again the second time.
As an example, consider the following methods from a temporary-handle class:
CTempHandle::CTempHandle( ODULong size ) {
fHandle = kODNULL;
fHandle = ODNewHandle(size);
}
CTempHandle::~CTempHandle(
ODHandle h = fHandle;
fHandle = kODNULL;
ODDisposeHandle(h);
}
The double assignment in the constructor is there so that, if ODNewHandle fails, the variable fHandle has a default value of NULL. Otherwise its value would be garbage and the destructor would crash. Similarly, the destructor first sets fHandle to NULL before calling ODDisposeHandle, in case an exception in ODDisposeHandle causes the destructor to be called again. (Actually, ODDisposeHandle never throws an exception, but for the sake of example we're assuming it might.)
Exceptions and SOM
The preceding discussion is common to most exception handling packages. However, we’ve added some special features to simplify working with SOM. The reasons why these features are necessary are:
1. SOM1 has its own way of returning error codes, based on an “Environment” variable, a pointer to which is passed into every SOM method. Methods you write and methods you call will return errors this way. (For more detail, see page 3-27 of the SOM manual.)
2. You cannot throw an exception, or allow one to be thrown, out of a SOM method. SOM requires that a method return normally, and throwing an exception that’s caught by a handler in some other function farther up the stack would violate this.
This implies that the ev parameter must be checked for an error status after every call to a SOM method; and that an exception raised in a SOM method or any function it calls must be caught and its error code stored in the ev parameter. Fortunately, the exception package includes utilities to make this fairly painless. You just need to use variants of the normal exception handler macros:
SOM_TRY
SOM_CATCH_ALL
SOM_ENDTRY
These macros are identical to the normal ones, except that , when they catch an exception, they store the exception value in the method's "ev" parameter where the caller will see it.
Since you cannot throw out of a SOM method, it is illegal to use RERAISE in the SOM_CATCH_ALL block. You should exit from the function normally, by falling off the end or calling return. (In C++, the former generates slightly better code.)
The example shows the implementation of the NewBaz method of the class ODFooBar. Since this method calls ODNewPtr, which throws an exception if it fails, it needs an exception handler. If ODNewPtr succeeds, the newly allocated Baz structure is initialized and returned. If, on the other hand, ODNewPtr throws an exception — whose error code most likely will be kODErrOutOfMemory — the exception handler will catch the exception, stuff the exception code into the ev parameter for the caller to see, and jump to the SOM_CATCH_ALL block. This sets baz to NULL so that NULL will be returned by the return statement. (The caller will probably ignore the return value upon seeing that an exception is set in the ev parameter, but it's best to return something safe anyway.)
IMPORTANT: SOM_ENDTRY works somewhat differently than ENDTRY in that its default is to “reraise” the exception by storing the error info in the Environment so it will be propagated to the caller. (With ENDTRY you must explicitly use RERAISE in your CATCH_ALL block or the exception will disappear.) If you don't want to return the exception to the caller, you must call SetErrorCode(kODNoError) in the SOM_CATCH block. You can also use SetErrorCode to return a different error code, if you ever find that necessary.
Automatic Environment Checking
Including the header Except.h defines a special preprocessor symbol that modifies the way SOM messages are sent. Any SOM headers (.xh files, for C++) included after Except.h will be modified so that, after the message is sent and control returns to the caller, the environment (ev) is checked and an exception raised if the method returned an error2 .
For example, here’s a code snippet that doesn’t use this functionality:
#include <ODFoo.xh>
...
long AFunction( Environment *ev, ODFoo *foo )
{
long result = foo->Bazz(ev);
if( ev->_major ) { // Oops, ODFoo::Bazz returned an error
result = 0;
goto handle_error;
}
...
handle_error:
return result;
}
In this example, Except.h is not included, so environment checking is not automatic, and the caller (AFunction) can and must check the environment after every call.
Here’s the same example with automatic environment checking:
#include <Except.h> // Enables automatic ev checking for ODFoo!
#include <ODFoo.xh>
...
long AFunction( Environment *ev, ODFoo *foo )
{
long result;
SOM_TRY
long result = foo->Bazz(ev);
...
SOM_CATCH_ALL
result = kODNULL;
SOM_ENDTRY
return result;
}
Since Except.h is included before ODFoo.h, invisible environment-checking code is added to the call to ODFoo’s Bazz method. If Bazz encounters an error and returns error status in ev, an exception with that same error code is thrown, which will be caught by the exception handler, and in its turn returned in the ev parameter.
There are two important caveats to keep in mind:
1. It’s important that you include Except.h before any headers that declare SOM classes (.xh files, in C++) if you want to use automatic environment checking. It will not take effect for any SOM classes that are declared before Except.h is included. Remember that this applies to precompiled headers, too. If you precompile any SOM headers (a nearly universal practice) you should also precompile Except.h and put it first in the header that you precompile.
2. When using automatic environment checking, any SOM method call may throw an exception , so any SOM method that calls other SOM methods must be prepared to handle exceptions, probably via a SOM_CATCH.